﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Isam.Esent.Interop;
using System.Diagnostics;
using System.ComponentModel;

namespace Microsoft.Isam.Esent.Serialization
{
	/// <summary>This class implements data source for a virtual-mode data grid view from windows forms.</summary>
	/// <remarks><b>Achtung:</b> the code in this class is practically untested.
	/// Use it on your own risk, be ready for debugging.</remarks>
	/// <typeparam name="tRow"></typeparam>
	// TODO [low] implement ErrGetApproximateRecordCount for filtered recordsets. Currently it takes the whole index, which looks like weird scrollbar behaviour.
	// TODO [low] cache several recently requested rows to improve performance. In the real-world scenarios I've tested, GetAt() method is called in random order about 100 times for a page showing ~7 displayed records with 3 fields each.
	public class VirtualMode<tRow> : IDisposable
		where tRow: new()
	{
		Recordset<tRow> m_rs;

		/// <summary>DB session</summary>
		public iSerializerSession session { get { return m_rs.session; } }
		/// <summary>DB cursor</summary>
		public Cursor<tRow> cursor { get { return m_rs.cursor; } }
		/// <summary>DB recordset</summary>
		public Recordset<tRow> recordset { get { return m_rs; } }

		/// <summary>Include the whole table.</summary>
		/// <param name="_sess"></param>
		public VirtualMode( iSerializerSession _sess )
		{
			_sess.getTable( out m_rs );
			BuildIndicesCache();
		}

		/// <summary>Include only the subset of records and/or reverse ordered. </summary>
		/// <param name="rs"></param>
		public VirtualMode( Recordset<tRow> rs )
		{
			m_rs = rs;
			BuildIndicesCache();
		}

		bool m_bCellValueNeededBound = false;

		/// <summary>Free the resources.</summary>
		public void Dispose()
		{
			if( m_bCellValueNeededBound && null != m_grid )
			{
				m_grid.CellValueNeeded -= dgView_CellValueNeeded;
				m_bCellValueNeededBound = false;
			}
		}

		/// <summary>Add the new item to the table.</summary>
		/// <param name="newItem"></param>
		public void Add( tRow newItem )
		{
			cursor.Add( newItem );
		}

		// You may want to use the indexing functionality of the class even without data grid view,
		// anytime you need reasonably accurate, fast, lazy, index-based records access for large tables.

		#region Indexing.

		// TODO [very low]: using sorted System.Collections.Generic.List<pair<int, ByteArray>> instead of the list might improve the preformance a bit.
		SortedList<int, ByteArray> m_bookmarks = new SortedList<int,ByteArray>();
		int m_CountApproximate = -1;
		int m_nLastCachedIndex = -1;
		int m_CountAccurate = -1;

		int m_iLastRequested = -2;	//< -2 to not try to MoveNext() from -1 to fetch the first record.
		tRow m_objLastRequested;
		ByteArray m_bkLastRequested;

		// Less value = faster random seeks, more RAM.
		// More value = slower random seeks, less RAM.
		const int s_indexFrequency = 64;

		bool m_bRecordsCountEstimated;

		const int nInitialRecordsToIndex = 50;
		const int nInvisibleRecords = 100;

		void BuildIndicesCache()
		{
			m_bookmarks.Clear();
			if( !m_rs.applyFilter() )
			{
				m_CountApproximate = m_CountAccurate = 0;
				return;
			}
			m_CountAccurate = -1;
			m_nLastCachedIndex = 0;
			m_bookmarks.Add( 0, m_rs.getFirstBookmark() );

			if( !m_rs.applyFilter() )
			{
				m_bRecordsCountEstimated = true;
				m_CountApproximate = m_CountAccurate = 0;
			}
			else
			{
				m_CountApproximate = -1;
				if( m_rs.canEstimateRecordsCount )
				{
					m_CountApproximate = m_rs.EstimateRecordsCount();
					m_bRecordsCountEstimated = true;
				}
				else
					m_bRecordsCountEstimated = false;

				m_CountAccurate = -1;
			}

			if( !m_bRecordsCountEstimated )
			{
				m_rs.applyFilter();
				for( int i = 1; i < nInitialRecordsToIndex; i++ )
				{
					if( !m_rs.tryMoveNext() )
					{
						// We moved to the end
						m_bRecordsCountEstimated = true;
						m_CountApproximate = m_CountAccurate = i;
						break;
					}
					CacheIndexIfNeeded( i );
				}
				if( !m_bRecordsCountEstimated )
					m_CountApproximate = nInitialRecordsToIndex + nInvisibleRecords;
			}
			
			m_iLastRequested = -2;	//< -2 to not try to MoveNext() from -1 to fetch the first record.
			m_objLastRequested = default( tRow );

			UpdateRowCount();
		}

		void CacheIndexIfNeeded( int i )
		{
			if( 0 == ( i % s_indexFrequency ) )
			{
				m_bookmarks.Add( i, cursor.getBookmark() );
				m_nLastCachedIndex = Math.Max( m_nLastCachedIndex, i );
			}
		}

		void onStepForwardFailed( int nIndexFailedToNavigate )
		{
			m_CountAccurate = m_CountApproximate = ( nIndexFailedToNavigate );
			m_bRecordsCountEstimated = true;
			BeginUpdateRowCountAsync();
		}

		// Navigate to the given bookmark of the posIndex item,
		// then step forward until the iRequested index is reached.
		// The intermediate bookmarks are cached as needed, to allow faster navigation the next time, if any.
		bool TryGotoIndex( int iRequested, int posIndex, ByteArray bkIndex )
		{
			Debug.Assert( iRequested >= posIndex );

			if( !m_rs.tryGotoBookmark( bkIndex ) )
				return false;

			if( iRequested == posIndex )
				return true;

			do
			{
				posIndex++;
				if( !m_rs.tryMoveNext() )
				{
					onStepForwardFailed( posIndex );
					return false;
				}
				CacheIndexIfNeeded( posIndex );
			}
			while( posIndex < iRequested );

			if( m_CountAccurate < 0 )
			{
				if( m_bRecordsCountEstimated ) 
				{
					int nRemained = ( m_CountApproximate - iRequested );
					if( nRemained <= 2 * s_indexFrequency )
					{
						// We're already indexed almost the whole table.
						// IMO it's a reasonable assumption indexing the rest of the data wont hurt performance too much.
						ByteArray bkRes = cursor.getBookmark();
						nRemained = m_rs.CountToEnd();
						m_rs.tryGotoBookmark( bkRes );
						m_CountAccurate = iRequested + nRemained;

						BeginUpdateRowCountAsync();
					}
				}
				else
				{
					m_CountApproximate = Math.Max( m_CountApproximate, posIndex + nInvisibleRecords );

					if( null != m_grid  )
						if( m_CountApproximate - m_grid.RowCount > nInvisibleRecords / 2 )
							BeginUpdateRowCountAsync();
				}
			}

			return true;
		}

		tRow FromIndex( int iRequested, int posIndex, ByteArray bkIndex )
		{
			if( !TryGotoIndex( iRequested, posIndex, bkIndex ) )
				return m_newRow;

			return cursor.getCurrent();
		}

		/// <summary>Navigate the cursor to the specified indexed position.</summary>
		/// <param name="nRecord">The continuous zero-based index to navigate.</param>
		/// <returns>On success, returns the bookmark of the destination record.
		/// On failure, returns ByteArray.Empty</returns>
		public ByteArray GotoBookmarkAt( int nRecord )
		{
			if( m_CountAccurate >= 0 )
				if( nRecord >= m_CountAccurate )
					return ByteArray.Empty;

			if( m_iLastRequested >= 0 )
			{
				if( nRecord == m_iLastRequested )
				{
					// Last requested element
					if( !m_rs.tryGotoBookmark( m_bkLastRequested ) )
						return ByteArray.Empty;

					return m_bkLastRequested;
				}

				if( nRecord == m_iLastRequested + 1 )
				{
					// Navigate one step forward from the last requested element
					if( !m_rs.tryGotoBookmark( m_bkLastRequested ) )
						Debug.Assert( false );
					if( !m_rs.tryMoveNext() )
					{
						onStepForwardFailed( nRecord );
						return ByteArray.Empty;
					}
					return cursor.getBookmark();
				}

				if( nRecord == m_iLastRequested - 1 )
				{
					// Navigate one step backward from the last requested element
					if( !m_rs.tryGotoBookmark( m_bkLastRequested ) )
						Debug.Assert( false );
					if( !m_rs.tryMovePrevious() )
						throw new IndexOutOfRangeException();
					return cursor.getBookmark();
				}
			}

			// We have an arbitrary search request,
			// which is actually our worst case in terms of the CPU ticks required.
			bool bSucceeded = false;
			if( nRecord <= m_nLastCachedIndex )
			{
				// At least we have a cached bookmark not far from the requested record.
				int posIndex = ( nRecord / s_indexFrequency ) * s_indexFrequency;
				bSucceeded = TryGotoIndex( nRecord, posIndex, m_bookmarks[ posIndex ] );
			}
			else
				// Here we don't have a cached bookmark, and are potentially forced to traverse through the dhole dataset :-(
				bSucceeded = TryGotoIndex( nRecord, m_nLastCachedIndex, m_bookmarks[ m_nLastCachedIndex ] );

			if( !bSucceeded )
				return ByteArray.Empty;

			return cursor.getBookmark();
		}

		/// <summary>Get the object from the specified index.</summary>
		/// <param name="nRecord">The continuous zero-based index to navigate.</param>
		/// <returns></returns>
		public tRow GetAt( int nRecord )
		{
			ByteArray bk = GotoBookmarkAt( nRecord );
			if( bk.isEmpty() )
				return m_newRow;

			m_iLastRequested = nRecord;
			m_objLastRequested = cursor.getCurrent();
			m_bkLastRequested = bk;

			return m_objLastRequested;
		}

		/// <summary>Get the records count.</summary>
		/// <returns>The value may be approximate.</returns>
		public int GetCount()
		{
			if( m_CountAccurate >= 0 )
				return m_CountAccurate;
			return m_CountApproximate;
		}
		#endregion

		#region DataGridView-specific
		readonly tRow m_newRow = new tRow();
		PropertyDescriptorCollection m_pdc = null;
		System.Windows.Forms.DataGridView m_grid = null;

		/// <summary>Bind to the <see cref="System.Windows.Forms.DataGridView" />.</summary>
		/// <remarks>If the data grid's AutoGenerateColumns property is set to true, this will also auto-generate columns,
		/// and wire the CellValueNeeded event to the internal event handler.</remarks>
		/// <param name="dgView"></param>
		public void BindToDataGrid( System.Windows.Forms.DataGridView dgView )
		{
			m_grid = dgView;

			m_grid.DataSource = null;
			m_grid.Rows.Clear();

			bool bAutoGeneratedColumns = m_grid.AutoGenerateColumns;
			if( bAutoGeneratedColumns )
			{
				// Fool DataGridView to make it auto-generate the columns
				m_grid.Columns.Clear();
				var tmpDataSource = new EditableObjectList<tRow>();
				m_grid.DataSource = tmpDataSource;
				m_grid.AutoGenerateColumns = false;
				m_grid.DataSource = null;
				m_grid.AutoGenerateColumns = true;
				m_pdc = TypeDescriptor.GetProperties( m_newRow );
			}
			m_grid.VirtualMode = true;

			// Wire up the events, if the columns were generated automatically.
			if( bAutoGeneratedColumns )
			{
				m_grid.CellValueNeeded += dgView_CellValueNeeded;
				m_bCellValueNeededBound = true;
			}

			UpdateRowCount();
		}

		// Now this is the tricky part.
		// The grid's row count is sometimes updated from the CellValueNeeded event handler.
		// This happens when the ESE first ****s up estimating records count using ErrGetApproximateRecordCount trick, then says "no more records" while trying to get a value.
		// If skip the 'm_grid.Rows.Clear()' call, the control with 1M records will update itself ~forever.
		// If call Rows.Clear() while calling UpdateRowCount() from within the CellValueNeeded() event handler, NullPointerException is thrown from deep inside winforms.
		// That's why UpdateRowCount requests are posted using control.BeginInvoke API.
		// Besides, we don't allow more then one UpdateRowCount request to be posted: since the whole application GUI lives in the single thread, the single posted update request is adequate.
		bool m_bPendingUpdateRowCount = false;
		void UpdateRowCount()
		{
			if( null == m_grid )
				return;
			int nRows = GetCount();

			int r = m_grid.FirstDisplayedScrollingRowIndex, c = m_grid.FirstDisplayedScrollingColumnIndex;
			m_grid.Rows.Clear();

			if( m_grid.AllowUserToAddRows )
				m_grid.RowCount = nRows + 1;	// The blank row for new record.
			else
				m_grid.RowCount = nRows;

			if( r > 0 ) m_grid.FirstDisplayedScrollingRowIndex = Math.Min( r, nRows - 1 );
			if( c > 0 ) m_grid.FirstDisplayedScrollingColumnIndex = c;
			m_bPendingUpdateRowCount = false;
		}

		void BeginUpdateRowCountAsync()
		{
			if( null == m_grid )
				return;
			int c = GetCount();
			if( c == 0 )
				return;

			if( m_bPendingUpdateRowCount )
				return;

			if( m_grid.IsDisposed ) return;

			m_bPendingUpdateRowCount = true;

			m_grid.BeginInvokeEx( this.UpdateRowCount );
		}

		void dgView_CellValueNeeded( object sender, System.Windows.Forms.DataGridViewCellValueEventArgs e )
		{
			tRow r;
			if( m_grid.NewRowIndex == e.RowIndex )
				r = m_newRow;
			else
				r = GetAt( e.RowIndex );

			string columnName = m_grid.Columns[e.ColumnIndex].DataPropertyName;
			e.Value = m_pdc[ columnName ].GetValue( r );
		}

		/// <summary>Refresh the control from the DB.</summary>
		public void Refresh()
		{
			/* 
			// Save selection, if any
			ByteArray bkSelection = ByteArray.Empty;
			try
			{
				if( m_grid.SelectedRows.Count > 0 )
				{
					GetAt( m_grid.SelectedRows[ 0 ].Index );
					bkSelection = m_bkLastRequested;
				}
			}
			catch( System.Exception )
			{
				// Swallow
			}

			// Save viewable area

			ByteArray bkFirstDisplayed = ByteArray.Empty;
			try
			{
				if( m_grid.FirstDisplayedScrollingRowIndex >= 0 )
				{
					GetAt( m_grid.FirstDisplayedScrollingRowIndex );
					bkFirstDisplayed = m_bkLastRequested;
				}
			}
			catch( System.Exception )
			{
				// Swallow
			} */

			// Clear everything.
			m_bookmarks.Clear();
			m_CountApproximate = -1;
			m_nLastCachedIndex = -1;
			m_CountAccurate = -1;

			m_iLastRequested = -2;	//< -2 to not try to MoveNext() from -1 to fetch the first record.
			m_objLastRequested = default( tRow );
			m_bkLastRequested = ByteArray.Empty;
			m_bRecordsCountEstimated = false;

			if( null != m_grid )
			{
				// I couldn't fucking believe it, however stupid DataGridView calls onCellValueNeeded from deep inside Rows.Clear.
				m_CountAccurate = 0;
				m_grid.Rows.Clear();
				m_grid.RowCount = 0;
				m_CountAccurate = -1;
			}

			BuildIndicesCache();

			// TODO: restore selection and/or viewable area from the bookmarks
		}
		#endregion
	}
}
